# NeHe Tutorial Lesson: 41 - Volumetric Fog
#
# Ported to PyOpenGL 2.0 by Brian Leair 18 Jan 2004
#
# This code was created by Jeff Molofee 2000
#
# The port was based on the PyOpenGL tutorials and from
# PyOpenGLContext (tests/glprint.py)
#
# If you've found this code useful, feel free to let me know
# at (Brian Leair telcom_sage@yahoo.com).
#
# See original source and C based tutorial at http://nehe.gamedev.net
#
# Note:
# -----
# This code is not an ideal example of Pythonic coding or use of OO techniques.
# It is a simple and direct exposition of how to use the Open GL API in
# Python via the PyOpenGL package. It also uses GLUT, a high quality
# platform independent library. Due to using these APIs, this code is
# more like a C program using procedural based programming.
#
# To run this example you will need:
# Python 	- www.python.org (v 2.3 as of 1/2004)
# PyOpenGL 	- pyopengl.sourceforge.net (v 2.0.1.07 as of 1/2004)
# Numeric Python	- (v.22 of "numpy" as of 1/2004) numpy.sourceforge.net
# Python Image Library	- http://www.pythonware.com/products/pil/
#
# Make sure to get versions of Numeric, PyOpenGL, and PIL to match your
# version of python.
#
#
#
#
# Topics demonstrated in this tutorial:
# using PIL (Python Image Library) to load a texture from an image file
# 		(see doc - http://www.pythonware.com/library/pil/handbook/index.htm)
#	accessing the extension FOG_COORDINATE_EXTENSION
# 		(see doc - http://pyopengl.sourceforge.net/documentation/opengl_diffs.html)
#

import sys

from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
import PIL.Image as Image  # PIL
from OpenGL.GL.EXT.fog_coord import *




# *********************** Globals ***********************
# Python 2.2 defines these directly
'''
try:
	True
except NameError:
	True = 1==1
	False = 1==0
'''

# Some api in the chain is translating the keystrokes to this octal string
# so instead of saying: ESCAPE = 27, we use the following.
ESCAPE = b'\033'

# Number of the glut window.
window = 0
fogColor = (0.6, 0.3, 0.0, 1.0)  # // Fog Colour
camz = None  # // Camera Z Depth
lastTickCount = 0.0

texture = None


def next_p2(num):
    """ If num isn't a power of 2, will return the next higher power of two """
    rval = 1
    while (rval < num):
        rval <<= 1
    return rval


def BuildTexture(path):
    """ // Load Image And Convert To A Texture
    path can be a relative path, or a fully qualified path.
    returns False if the requested image couldn't loaded as a texture
    returns True and the texture ID if image was loaded
    """
    # Catch exception here if image file couldn't be loaded
    try:
        # Note, NYI, path specified as URL's could be access using python url lib
        # OleLoadPicturePath () supports url paths, but that capability isn't critcial to this tutorial.
        Picture = Image.open(path)
    except:
        print("Unable to open image file '%s'." % (path))
        return False, 0

    glMaxTexDim = glGetIntegerv(GL_MAX_TEXTURE_SIZE)

    WidthPixels = Picture.size[0]
    HeightPixels = Picture.size[1]

    if ((WidthPixels > glMaxTexDim) or (HeightPixels > glMaxTexDim)):
        # The image file is too large. Shrink it to fit within the texture dimensions
        # support by our rendering context for a GL texture.
        # Note, Feel free to experiemnt and force a resize by placing a small val into
        # glMaxTexDim (e.g. 32,64,128).
        if (WidthPixels > HeightPixels):
            # Width is the domainant dimension.
            resizeWidthPixels = glMaxTexDim
            squash = float(resizeWidthPixels) / float(WidthPixels)
            resizeHeightPixels = int(HeightPixels * squash)
        else:
            resizeHeightPixels = glMaxTexDim
            squash = float(resizeHeightPixels) / float(HeightPixels)
            resizeWidthPixels = int(WidthPixels * squash)
    else:
        # // Resize Image To Closest Power Of Two
        if (WidthPixels > HeightPixels):
            # Width is the domainant dimension.
            resizeWidthPixels = next_p2(WidthPixels)
            squash = float(resizeWidthPixels) / float(WidthPixels)
            resizeHeightPixels = int(HeightPixels * squash)
        else:
            resizeHeightPixels = next_p2(HeightPixels)
            squash = float(resizeHeightPixels) / float(HeightPixels)
            resizeWidthPixels = int(WidthPixels * squash)
        #
    # Resize the image to be used as a texture.
    # The Python image library provides a handy method resize ().
    # Several filtering options are available.
    # If you don't specify a filtering option will default NEAREST
    Picture = Picture.resize((resizeWidthPixels, resizeHeightPixels), Image.BICUBIC)
    lWidthPixels = next_p2(resizeWidthPixels)
    lHeightPixels = next_p2(resizeWidthPixels)
    # Now we create an image that has the padding needed
    newpicture = Image.new("RGB", (lWidthPixels, lHeightPixels), (0, 0, 0))
    newpicture.paste(Picture)

    # Create a raw string from the image data - data will be unsigned bytes
    # RGBpad, no stride (0), and first line is top of image (-1)
    pBits = newpicture.tostring("raw", "RGBX", 0, -1)

    # // Typical Texture Generation Using Data From The Bitmap
    texid = glGenTextures(1)  # // Create The Texture
    glBindTexture(GL_TEXTURE_2D, texid)  # // Bind To The Texture ID
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR)  # // (Modify This For The Type Of Filtering You Want)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
                    GL_LINEAR)  # // (Modify This For The Type Of Filtering You Want)

    # // (Modify This If You Want Mipmaps)
    glTexImage2D(GL_TEXTURE_2D, 0, 3, lWidthPixels, lHeightPixels, 0, GL_RGBA, GL_UNSIGNED_BYTE, pBits)

    # Cleanup (python actually handles all memory for you, so this isn't necessary)
    # // Decrements IPicture Reference Count
    Picture = None
    newpicture = None
    return True, texid  # // Return True (All Good)


def Extension_Init():
    """ Determine if the fog coord extentsion is availble """

    # After calling this, we will be able to invoke glFogCoordEXT ()
    if (not glInitFogCoordEXT()):
        print("Help!  No GL_EXT_ForCoord")
        sys.exit(1)

    return True


# // Any GL Init Code & User Initialiazation Goes Here
def InitGL(Width, Height):  # We call this right after our OpenGL window is created.
    global fogColor
    global camz

    if (not Extension_Init()):  # // Check And Enable Fog Extension If Available
        return False  # // Return False If Extension Not Supported

    if (not BuildTexture("Wall.bmp")):  # // Load The Wall Texture
        return False  # // Return False If Loading Failed

    glEnable(GL_TEXTURE_2D)  # // Enable Texture Mapping
    glClearColor(0.0, 0.0, 0.0, 0.5)  # // Black Background
    glClearDepth(1.0)  # // Depth Buffer Setup
    glDepthFunc(GL_LEQUAL)  # // The Type Of Depth Testing
    glEnable(GL_DEPTH_TEST)  # // Enable Depth Testing
    glShadeModel(GL_SMOOTH)  # // Select Smooth Shading
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)  # // Set Perspective Calculations To Most Accurate

    # // Set Up Fog
    glEnable(GL_FOG)  # // Enable Fog
    glFogi(GL_FOG_MODE, GL_LINEAR)  # // Fog Fade Is Linear
    glFogfv(GL_FOG_COLOR, fogColor)  # // Set The Fog Color
    glFogf(GL_FOG_START, 0.0)  # // Set The Fog Start
    glFogf(GL_FOG_END, 1.0)  # // Set The Fog End
    glHint(GL_FOG_HINT, GL_NICEST)  # // Per-Pixel Fog Calculation
    glFogi(GL_FOG_COORDINATE_SOURCE_EXT, GL_FOG_COORDINATE_EXT)  # // Set Fog Based On Vertice Coordinates

    camz = -19.0  # // Set Camera Z Position To -19.0f

    return True  # // Return TRUE (Initialization Successful)


# The function called when our window is resized (which shouldn't happen if you enable fullscreen, below)
def ReSizeGLScene(Width, Height):
    if Height == 0:  # Prevent A Divide By Zero If The Window Is Too Small
        Height = 1

    glViewport(0, 0, Width, Height)  # Reset The Current Viewport And Perspective Transformation
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    # // field of view, aspect ratio, near and far
    # This will squash and stretch our objects as the window is resized.
    gluPerspective(45.0, float(Width) / float(Height), 0.1, 100.0)

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()


# The main drawing function.
def DrawGLScene():
    global camz

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)  # // Clear Screen And Depth Buffer
    glLoadIdentity()  # // Reset The Modelview Matrix

    # glTranslatef(0.0, 0.0, camz)  # // Move To Our Camera Z Position
    '''
    glBegin(GL_QUADS)  # // Back Wall
    glFogCoordfEXT(1.0)
    glTexCoord2f(0.0, 0.0)
    glVertex3f(-2.5, -2.5, -15.0)
    glFogCoordfEXT(1.0)
    glTexCoord2f(1.0, 0.0)
    glVertex3f(2.5, -2.5, -15.0)
    glFogCoordfEXT(1.0)
    glTexCoord2f(1.0, 1.0)
    glVertex3f(2.5, 2.5, -15.0)
    glFogCoordfEXT(1.0)
    glTexCoord2f(0.0, 1.0)
    glVertex3f(-2.5, 2.5, -15.0)
    # PyOpenGL 2.0.1.07 has a bug. Swig generated code for FogCoordfExt ()
    # uses wrong error check macro. Macro falsely sets exception due to
    # call during glBegin (). Fixed in later versions.
    try:
        glEnd()
    except:
        pass
    '''

    # DrawTunnel()
    glBegin(GL_QUADS)  # // Floor
    glFogCoordfEXT(1.0)
    glTexCoord2f(0.0, 0.0)
    glVertex3f(-2.5, -2.5, -15.0)
    glFogCoordfEXT(1.0)
    glTexCoord2f(1.0, 0.0)
    glVertex3f(2.5, -2.5, -15.0)
    glFogCoordfEXT(0.0)
    glTexCoord2f(1.0, 1.0)
    glVertex3f(2.5, -2.5, 15.0)
    glFogCoordfEXT(0.0)
    glTexCoord2f(0.0, 1.0)
    glVertex3f(-2.5, -2.5, 15.0)
    try:
        glEnd()
    except:
        pass

    glBegin(GL_QUADS)  # // Roof
    glFogCoordfEXT(1.0)
    glTexCoord2f(0.0, 0.0)
    glVertex3f(-2.5, 2.5, -15.0)
    glFogCoordfEXT(1.0)
    glTexCoord2f(1.0, 0.0)
    glVertex3f(2.5, 2.5, -15.0)
    glFogCoordfEXT(0.0)
    glTexCoord2f(1.0, 1.0)
    glVertex3f(2.5, 2.5, 15.0)
    glFogCoordfEXT(0.0)
    glTexCoord2f(0.0, 1.0)
    glVertex3f(-2.5, 2.5, 15.0)
    try:
        glEnd()
    except:
        pass

    glBegin(GL_QUADS)  # // Right Wall
    glFogCoordfEXT(0.0)
    glTexCoord2f(0.0, 0.0)
    glVertex3f(2.5, -2.5, 15.0)

    glFogCoordfEXT(0.0)
    glTexCoord2f(0.0, 1.0)
    glVertex3f(2.5, 2.5, 15.0)

    glFogCoordfEXT(1.0)
    glTexCoord2f(1.0, 1.0)
    glVertex3f(2.5, 2.5, -15.0)

    glFogCoordfEXT(1.0)
    glTexCoord2f(1.0, 0.0)
    glVertex3f(2.5, -2.5, -15.0)
    try:
        glEnd()
    except:
        pass

    glBegin(GL_QUADS)  # // Left Wall

    glFogCoordfEXT(0.0)
    glTexCoord2f(0.0, 0.0)
    glVertex3f(-2.5, -2.5, 15.0)

    glFogCoordfEXT(0.0)
    glTexCoord2f(0.0, 1.0)
    glVertex3f(-2.5, 2.5, 15.0)

    glFogCoordfEXT(1.0)
    glTexCoord2f(1.0, 1.0)
    glVertex3f(-2.5, 2.5, -15.0)

    glFogCoordfEXT(1.0)
    glTexCoord2f(1.0, 0.0)
    glVertex3f(-2.5, -2.5, -15.0)
    try:
        glEnd()
    except:
        pass

    glutSwapBuffers()  # // Flush The GL Rendering Pipeline
    return True


# The function called whenever a key is pressed. Note the use of Python tuples to pass in: (key, x, y)
def keyPressed(*args):
    global window
    global camz
    global lastTickCount

    tickCount = glutGet(GLUT_ELAPSED_TIME)
    milliseconds = (tickCount - lastTickCount)
    lastTickCount = tickCount
    if (milliseconds > 200):
        lastTickCount = tickCount
        milliseconds = 20

    # If escape is pressed, kill everything.
    if args[0] == ESCAPE:
        sys.exit()
    if ((args[0] == GLUT_KEY_UP) and (camz < 14.0)):
        camz += milliseconds / 100.0  # // Move Object Closer (Move Forwards Through Hallway)
    if (args[0] == GLUT_KEY_DOWN and camz > -29.0):
        camz -= milliseconds / 100.0  # // Move Object Closer (Move Backwards Through Hallway)

    return


def main():
    global window, lastTickCount

    # pass arguments to init
    glutInit(sys.argv)

    # Select type of Display mode:
    #  Double buffer
    #  RGBA color
    # Alpha components supported
    # Depth buffer
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH)

    # get a 640 x 480 window
    glutInitWindowSize(640, 480)

    # the window starts at the upper left corner of the screen
    glutInitWindowPosition(0, 0)

    # Okay, like the C version we retain the window id to use when closing, but for those of you new
    # to Python, remember this assignment would make the variable local and not global
    # if it weren't for the global declaration at the start of main.
    window = glutCreateWindow("NeHe's Volumetric Fog & IPicture Image Loading Tutorial")

    # Register the drawing function with glut, BUT in Python land, at least using PyOpenGL, we need to
    # set the function pointer and invoke a function to actually register the callback, otherwise it
    # would be very much like the C version of the code.
    glutDisplayFunc(DrawGLScene)

    # Uncomment this line to get full screen.
    #glutFullScreen()

    # When we are doing nothing, redraw the scene.
    glutIdleFunc(DrawGLScene)

    # Register the function called when our window is resized.
    glutReshapeFunc(ReSizeGLScene)

    # Register the function called when the keyboard is pressed.
    # The call setup glutSpecialFunc () is needed to receive
    # "keyboard function or directional keys."
    glutKeyboardFunc(keyPressed)
    glutSpecialFunc(keyPressed)

    # We've told Glut the type of window we want, and we've told glut about
    # various functions that we want invoked (idle, resizing, keyboard events).
    # Glut has done the hard work of building up thw windows DC context and
    # tying in a rendering context, so we are ready to start making immediate mode
    # GL calls.
    # Call to perform inital GL setup (the clear colors, enabling modes, and most releveant -
    # consturct the displays lists for the bitmap font.
    InitGL(640, 480)

    # Start Event Processing Engine
    glutMainLoop()

# Print message to console, and kick off the main to get it rolling.
if __name__ == "__main__":
    print("Hit ESC key to quit.")
    main()
